package com.ipemon.server;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;

import com.ipemon.exception.DataNotSentException;
import com.ipemon.model.Logger;

/**
 * @author surendra
 * Implements a pseudo TCP over UDP. The protocol functions as follows:
 * 
 *  A ==== 10 ===========> B
 *  A <=== 11 ============ B
 *  A ==== 20:1:DataY ===> B
 *  A <=== 21:1 ========== B
 *  A ==== 20:2:DataX ===> B
 *  A <=== 21:2 ========== B
 *  A ==== 00 ===========> B
 *  
 *  A ==== 10 ===========> B
 *  A <=== 11 ============ B
 *  A ==== 20:1:DataY ===> B
 *  A <=== 21:1 ========== B
 *  A ==== 20:2:DataX ===> B
 *  A <=== 21:1 ========== B
 *  A ==== 20:1:DataY ===> B
 *  A <=== 21:1 ========== B
 *  A ==== 20:2:DataX ===> B
 *  A <=== 21:2 ========== B
 *  A ==== 00 ===========> B
 *  
 *  The datastream that goes from one to another is in the following format.
 *  EE:CC:DATA
 *  where EE is a number representing the control protocol, and CC is the counter used in DATA mode and DATA is the 
 *  data itself being transfered.
 *  
 *  Control protocol:
 *  10 -> Connect
 *  11 -> ACK Connect
 *  20:1 -> Data transfer seq 1
 *  21:1 -> ACK Data received seq 1
 *  23:1 -> Data transfer seq reset and sending from beginnng
 *  24 -> Data seq re-transfer request
 *  30 -> Connection close
 *  00:MSG -> Undefined behavior / Going to sleep 
 *
 */
public class PseudoTCPoverUDP implements Runnable {
	
	private DatagramSocket socket = null;
	
	private InetAddress distantAddress = null;
	private int distantPort = 0;

	private boolean receive = false;
	private boolean send = false;
	private boolean dataSent = false;
	private boolean outsiderWakeUp = false;
	
	private final int SLEEP = 0;
	private final int AWAKE_TO_CONNECT = 2;
	private final int WAIT_CONNECT_ACK = 3;
	private final int DATA_SENDING = 4;
	private final int DATA_ACK_WAIT = 5;
	private final int AWAKE_TO_LISTEN = 7;
	private final int WAIT_DATA = 8;
	private final int TREAT_DATA = 9;

	private int current_state = SLEEP;
	private int counter = 0;
	private List<String> sendingDataBuffer = new ArrayList<String>();
	private List<String> receivedDataBuffer = new ArrayList<String>();
	
	private static PseudoTCPoverUDP firstSelfInstance = null;
	private static int listeningPort1 = 20000; //by default
	
	private static PseudoTCPoverUDP secondSelfInstance = null;
	private static int listeningPort2 = 40000; //by default

	private static PseudoTCPoverUDP thirdSelfInstance = null;
	private static int listeningPort3 = 60000; //by default
	
	private Thread transportThread = null;
	private CallBack callback = null;
	
	private PseudoTCPoverUDP(int UDPport) throws SocketException {
		socket = new DatagramSocket(UDPport);
		
		transportThread = new Thread(this, "Transport");
		transportThread.start();
		log("PseudoTCP initialized");
	}
	
	public void closeAll() {
		transportThread.interrupt();
		socket.close();
	}
	
	public static void setListeningPort1(int port) throws SocketException {
		PseudoTCPoverUDP.listeningPort1 = port;
		if(firstSelfInstance==null)
			firstSelfInstance = new PseudoTCPoverUDP(port);
	}
	
	public static PseudoTCPoverUDP getFirstInstance() {
		if(PseudoTCPoverUDP.firstSelfInstance==null)
			try {
				
				PseudoTCPoverUDP.firstSelfInstance = new PseudoTCPoverUDP(listeningPort1);
				
			} catch (SocketException e) {
				e.printStackTrace();
			}
		return PseudoTCPoverUDP.firstSelfInstance;
	}
	
	public static void setListeningPort2(int port) throws SocketException {
		PseudoTCPoverUDP.listeningPort2 = port;
		if(secondSelfInstance==null)
			secondSelfInstance = new PseudoTCPoverUDP(port);
	}

	
	public static PseudoTCPoverUDP getSecondInstance() {
		if(PseudoTCPoverUDP.secondSelfInstance==null)
			try {
				
				PseudoTCPoverUDP.secondSelfInstance = new PseudoTCPoverUDP(listeningPort2);
				
			} catch (SocketException e) {
				e.printStackTrace();
			}
		return PseudoTCPoverUDP.secondSelfInstance;
	}
	
	public void close() {
		socket.close();
	}

	public void sendData(InetAddress address, int port, String data) throws IOException, DataNotSentException {
		List<String> list = new ArrayList<String>();
		list.add(data);
		sendData(address, port, list);
	}
	
	public void sendData(String data) throws IOException, DataNotSentException  {
		if(distantAddress !=null && distantPort != 0)
			sendData(distantAddress, distantPort, data);
		else
			throw new DataNotSentException();
	}

	
	public synchronized void sendData(InetAddress address, int port, List<String> data) throws IOException, DataNotSentException {
		log("Data sending initialized");
		
		distantAddress = address;
		distantPort = port;
		sendingDataBuffer = data;
		
		initiateSendingData();
		
//		receive = true;
		send = true;
		
		log("Transport thread notified, and waiting for it to notify");

		outsiderWakeUp = false;
		
		while(!outsiderWakeUp) {
			try {
				wait();
				//Thread.currentThread().sleep(100);
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
			
		send = false;
		sendingDataBuffer = null;
		
		if(dataSent)
			log("Data sending terminated");
		
		else 
			throw new DataNotSentException();

		return;
	}
	
	private void initiateSendingData() throws DataNotSentException {
		current_state = AWAKE_TO_CONNECT;
		
		try {
			
			post("10\n", distantAddress, distantPort);
			
		} catch (IOException e) {
			throw new DataNotSentException();
		}
		
		current_state = WAIT_CONNECT_ACK;
	}
	
	private void resetReceivedData() {
		receivedDataBuffer = null;
		receivedDataBuffer = new ArrayList<>();
	}
	
	public synchronized void receiveData(CallBack callback) throws IOException {
		receive = true;
		this.callback = callback;
//		notify(); //notifies the transport thread
		return;
	}
	
	
	private synchronized void pseudoTCP(DatagramPacket packet) throws IOException, DataNotSentException  {
		
		if(send) {
			log("Transport thread starting sending job");
			pseudoTCPsendingBehavior(packet);
			dataSent = true;
			outsiderWakeUp = true;
			send = false;
			
			notifyAll();
			
			log("Transport thread finished sending job and waiting threads notified");
			
		} else if (receive) {
			pseudoReceivingBehavior(packet);
			log("Receiving termininated, now calling callback");
			
			if(callback != null) {
				callback.callIfReceived(CallBack.DATA, receivedDataBuffer);
			}
			else {
				log("Callback not defined");
				
			}
		}
	}
	
	
	//precondition for this method is that the connection "10" must be already initiated before 
	private void pseudoTCPsendingBehavior(DatagramPacket packet) throws IOException, DataNotSentException {
		
		String data = new String(packet.getData());
		String msgType = data.substring(0, 2);
		
		log("Received: " + data);
		
		// Checks if ACK_CONNECT and proceed to transfer the first sequence of data
		if(msgType.equals("11") && this.current_state == WAIT_CONNECT_ACK) {
			current_state = DATA_SENDING;
			counter = 0;
			
			String data_stream = "20:" + ++counter + ":" + sendingDataBuffer.get(0) + "\n";
			
			post(data_stream, packet.getAddress(), packet.getPort());
			
			current_state = DATA_ACK_WAIT;
	
			pseudoTCPsendingBehavior(listenPacket());
			return;
		}
		
		// Checks if ACK_DATA and send the next sequence of data or terminates the connection
		if(msgType.equals("21") && this.current_state == DATA_ACK_WAIT) {
			
			data = data.substring(3);
			String sqStr = data.substring(0, data.indexOf(":"));
			String dataStream = "";
			
			if(Integer.valueOf(sqStr) == counter) {
				
				if(sendingDataBuffer.size() > counter) {
					current_state = DATA_SENDING;
					
					dataStream = "20:" + (counter+1) + ":" + sendingDataBuffer.get(counter++);
					
					post(dataStream, packet.getAddress(), packet.getPort());
					
					current_state = DATA_ACK_WAIT;
					pseudoTCPsendingBehavior(listenPacket());
					
				} else {
					
					dataStream = "30:Data transfer done, close connection\n";
					post(dataStream, packet.getAddress(), packet.getPort());
					this.current_state = SLEEP;
					return;
					
				}

			} 
			
			// means the proper sequence was not received at the destination so reset the data transfer and 
			// send it over again.
			else {
				
				this.current_state = DATA_SENDING;
				counter = 0;
				dataStream = "23:" + (counter+1) + sendingDataBuffer.get(counter++) + "\n";
				post(dataStream, packet.getAddress(), packet.getPort());

				this.current_state = DATA_ACK_WAIT;
				pseudoTCPsendingBehavior(listenPacket());
				
			}
			return;
		}
		
		else {
			
			if(this.current_state == DATA_ACK_WAIT) {
				this.current_state = DATA_SENDING;
				counter = 0;
				String dataStream = "23:" + (counter+1) + sendingDataBuffer.get(counter++) + "\n";
				post(dataStream, packet.getAddress(), packet.getPort());

				this.current_state = DATA_ACK_WAIT;
				pseudoTCPsendingBehavior(listenPacket());
				return;
			}
			
			if (current_state == SLEEP) 
				return;
			
			this.current_state = SLEEP;
			String sleeping = "00:Language mismatch, going to sleep, sending incomplete\n";
			post(sleeping, packet.getAddress(), packet.getPort());
			
			
			throw new DataNotSentException();
		}

	}
	
	private void pseudoReceivingBehavior(DatagramPacket packet) throws IOException {
		
		String data = new String(packet.getData());
		String msgType = data.substring(0, 2);
		
		log("Received: " + data);
		
		// Receive Connect request and send connect ACK
		if(msgType.equals("10") && this.current_state == SLEEP) {
			
			current_state = AWAKE_TO_LISTEN;
			
			String connect_ack = "11\n";
			post(connect_ack, packet.getAddress(), packet.getPort());
			
			current_state = WAIT_DATA;
			counter = 0;
			
			distantAddress = packet.getAddress();
			distantPort = packet.getPort();
			resetReceivedData();
			
			pseudoReceivingBehavior(listenPacket());
			return;
		}
		
		// Received first sequence of data and send DATA_ACK 
		if( (msgType.equals("20") || msgType.equals("23"))
				&& this.current_state == WAIT_DATA) {

			this.current_state = TREAT_DATA;
			
			if(msgType.equals("23")) {
				counter = 0;
				resetReceivedData();
			}
			
			data = data.substring(3);
			String sqStr = data.substring(0, data.indexOf(":"));
			
			String replyStr = "";
			if(Integer.valueOf(sqStr) - 1 == counter ) {
				//treat data
				receivedDataBuffer.add(counter, data.substring(data.indexOf(":")+1));
				
				//send acknowledgement
				++counter;
				replyStr = "21:" + String.valueOf(counter) + ":" + "\n";
				this.current_state = WAIT_DATA;
				
				
			} else {
				//send reset request
				replyStr = "24:" + String.valueOf(counter) + ":" + "\n";
				this.current_state = WAIT_DATA;
				counter = 0;
				resetReceivedData();
			}
			
			post(replyStr, packet.getAddress(), packet.getPort());
	
			pseudoReceivingBehavior(listenPacket());
			return;
		}
		
		if( msgType.equals("30") && this.current_state == WAIT_DATA) {
			this.current_state = SLEEP;
			counter = 0;
			return;
		}
		
		
		else {
			
			this.current_state = SLEEP;
			counter = 0;
			String sleeping = "00:Language mismatch, going to sleep, receiving incomplete\n";
			post(sleeping, packet.getAddress(), packet.getPort());
			
			return;
		}
	}
	
	private void post(String data, InetAddress address, int port) throws IOException {
		byte[] reply = data.getBytes();
		
		DatagramPacket replyPacket = new DatagramPacket(reply, reply.length, address, port);
		socket.send(replyPacket);
		
		log("Sent: " + data);
	}
	
	private DatagramPacket listenPacket() throws IOException {
		byte[] buffer = new byte[256];
		DatagramPacket arrivingPacket = new DatagramPacket(buffer, buffer.length);
		socket.receive(arrivingPacket);
		return arrivingPacket;
	}

	@Override
	public void run() {
			
		try {
			
			log("Now Listening for new DATA");
			
			pseudoTCP(listenPacket());

		} catch (IOException e) {
			e.printStackTrace();
			dataSent = false;
			
		} catch (DataNotSentException e) {
			e.printStackTrace();
			dataSent = false;
		}
		
		run();
	}

	private void log(String log){
		Logger.getInstance().log(this, log);
	}
	
	public void testSender() throws IOException, DataNotSentException {
		
		
		List<String> data = new ArrayList<>();
		data.add("First 1");
		/*		data.add("Second 2");
		data.add("Third 3");
		data.add("Fourth 4");
		data.add("Fifth 5");*/
		
		sendData(InetAddress.getByName("localhost"), 9090, data);
		
	}
	
	public static void main(String[] args) throws SocketException, IOException, DataNotSentException {
		
		Logger.getInstance().setStack(true);
		CallBack callback = new CallBack() {

			@Override
			public void callIfReceived(int type, Object data1) {
				List<String> data = (List<String>) data1;
				System.out.println("Strings received : " + data.size());
				for(int i=0; i<data.size(); i++) {
					System.out.println(data.get(i));
				}

				
			}

			
		};
		
			
			PseudoTCPoverUDP channel;
			PseudoTCPoverUDP.setListeningPort1(8080);
			
			channel = PseudoTCPoverUDP.getFirstInstance();
			channel.receiveData(callback);
			//channel.testSender();
			
			Logger.getInstance().log("all over");
			
		
		
	}

	
		
	
}
